use std::{fs, io};
use std::collections::HashMap;
use std::fs::copy;
use std::hash::Hasher;
use std::io::{BufReader, Error, Read, Write};
use std::path::Path;
use walkdir::{DirEntry, WalkDir};
use md5::{Context, Digest};

#[derive(serde::Serialize, Debug)]
pub struct FileInfo {
    pub path: String,
    pub size: String,
    pub size_num: u64,
}

pub fn visit_dir_first(dir: &Path) -> Vec<FileInfo> {
    let mut list: Vec<FileInfo> = Vec::new();
    if let Ok(rds) = fs::read_dir(dir) {
        for entryr in rds {
            if let Ok(entry) = entryr {
                let path = entry.path();
                if path.is_dir() {
                    let fi = FileInfo {
                        path: path.file_name().unwrap().to_str().unwrap().to_string(),
                        size: "0".to_string(),
                        size_num: 0,
                    };
                    list.push(fi);
                }
            }
        }
    }
    return list;
}

const SKIP_DIR: [&str; 6] =
    ["$WinREAgent", "ProgramData", "Recovery", "Windows", "AppData", "pkg"];

fn should_skip(entry: &DirEntry) -> bool {
    if !entry.path().starts_with("C:\\") && !entry.path().starts_with("c:\\") {
        return false;
    }
    if let Ok(e) = entry.metadata() {
        if e.is_file() {
            false
        } else {
            entry.file_name()
                .to_str()
                .map(|s| {
                    if SKIP_DIR.contains(&s) {
                        return true;
                    } else {
                        return s.starts_with(".");
                    }
                }).unwrap_or(false)
        }
    } else {
        false
    }
}

pub fn find_large_files(paths: Vec<String>) -> Vec<FileInfo> {
    let mut vec: Vec<FileInfo> = Vec::new();
    for path in paths {
        let mut list = walk_dir(path);
        vec.append(&mut list)
    }
    vec.sort_by(|ff, fl| fl.size_num.cmp(&ff.size_num));
    return vec;
}

pub fn walk_dir(path: String) -> Vec<FileInfo> {
    let mut list: Vec<FileInfo> = Vec::new();
    let min_size = 1024 * 1024 * 50;
    let walker = WalkDir::new(&path).max_depth(20).sort_by_file_name().into_iter();
    for entry in walker.filter_entry(|e| !should_skip(e)) {
        match entry {
            Ok(f) => {
                if let Ok(mta) = f.metadata() {
                    if mta.is_dir() {
                        continue;
                    }
                    let size = mta.len();
                    if min_size > 0 {
                        if size < min_size {
                            continue;
                        }
                    }
                    let fpath = f.path().to_str().unwrap().to_string();
                    println!("{:?}: {} bytes", fpath, size);
                    let fi = FileInfo {
                        path: fpath,
                        size: size_cal(size),
                        size_num: size,
                    };
                    list.push(fi);
                }
            }
            Err(e) => {
                println!("err>> {}", e);
            }
        }
    }
    return list;
}

#[cfg(test)]
mod test_find {
    use std::time::Instant;

    use crate::filefind::{find_large_files, find_repeat_files};

    #[test]
    pub fn test_walk_dir() {
        let startt = Instant::now();
        let p = vec!(String::from("c:\\Java"));
        let finds = find_repeat_files(p);
        println!("finds:{:?}", finds);
        // visit_dirs(&mut vec, p, 1024 * 1024 * 50_u64);
        let endt = Instant::now();
        println!("t>{}", endt.duration_since(startt).as_millis())
    }
}

pub fn find_repeat_files(dirs: Vec<String>) -> HashMap<String, Vec<FileInfo>> {
    let mut file_map: HashMap<String, Vec<FileInfo>> = HashMap::new();
    let min_size = 1024 * 1024 * 50;
    for path in dirs {
        let p = Path::new(&path);
        if !p.exists() {
            println!("文件夹不存在");
            continue;
        }
        let walker = WalkDir::new(&path).max_depth(20).sort_by_file_name().into_iter();
        for entry in walker.filter_entry(|e| !should_skip(e)) {
            match entry {
                Ok(f) => {
                    if let Ok(mta) = f.metadata() {
                        if mta.is_dir() {
                            continue;
                        }
                        let size = mta.len();
                        if min_size > 0 {
                            if size > min_size {
                                continue;
                            }
                        }
                        let size = mta.len();
                        let fpath = f.path().to_str().unwrap().to_string();
                        println!("{:?}: {} bytes", &fpath, size);
                        let fi = FileInfo {
                            path: fpath.clone(),
                            size: size_cal(size),
                            size_num: 0,
                        };
                        match hash_file(f.path()) {
                            Ok(file_hash) => {
                                file_map.entry(file_hash).or_insert(Vec::new()).push(fi);
                            }
                            Err(e) => {
                                println!("hash file err: {}", e)
                            }
                        };
                    }
                }
                Err(e) => {
                    println!("err>> {}", e);
                }
            }
        }
    }
    file_map.retain(|k, vs| {
        return vs.len() >= 2;
    });
    return file_map;
}

pub fn del_files(paths: Vec<String>) -> io::Result<()> {
    for path in paths {
        let ipath = Path::new(&path);
        if ipath.exists() {
            fs::remove_file(ipath)?;
        }
    }
    Ok(())
}

pub fn copy_file(paths: Vec<String>, output_path: String, del: bool) -> Result<(), Error> {
    let outdir = Path::new(&output_path);
    if !outdir.exists() {
        fs::create_dir_all(outdir)?;
    }
    for input_path in paths {
        let ipath = Path::new(&input_path);
        if !ipath.is_file() {
            println!("<< not file ok {} ", ipath.display());
            continue;
        }
        if let Some(fname) = ipath.file_name() {
            copy(ipath, outdir.join(fname))?;
            println!("copy ok {} > {}", ipath.display(), outdir.display());
            if del {
                fs::remove_file(ipath)?;
                println!("del ok {} ", ipath.display());
            }
        }
    }
    Ok(())
}

fn hash_file(file_path: &Path) -> io::Result<String> {
    let file = fs::File::open(file_path)?;
    let mut reader = BufReader::new(file);
    let mut hasher = Context::new();
    let mut buffer = [0; 1024];
    loop {
        let bytes_read = reader.read(&mut buffer)?;
        if bytes_read == 0 {
            break;
        }
        hasher.write_all(&buffer[..bytes_read])?;
    }
    let s = format!("{:x}", hasher.compute());
    Ok(s)
}

fn size_cal(size: u64) -> String {
    let mut s = 0.0;
    let mut u = "B";
    let sizef = size as f64;
    if size >= 1024 * 1024 * 1024 {
        //g
        s = sizef / (1024.000 * 1024.000 * 1024.000);
        u = "G"
    } else if size >= 1024 * 1024 {
        //m
        s = sizef / (1024.000 * 1024.000);
        u = "M"
    } else if size >= 1024 {
        //k
        s = sizef / 1024.000;
        u = "K"
    } else {
        //t
        s = sizef / 8.000
    }
    return format!("{:.2}{}", s, u);
}